#!/usr/bin/env python
#
# tmtoemacs - (attempt to) convert a textMate theme to an emacs color-theme
# 
# Copyright (c) 2013 Larry Daffner <vizzie.tx@gmail.com

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

from __future__ import print_function
from plistlib import readPlist
import matplotlib
import numpy as np
import sys
import re
from optparse import OptionParser

debug = False
faceformat='    %s(%s ((t (%s))))'

# A dictionary of mappings between Emacs faces and textMate scopes
emtotm = {
    'default' :'default',
    'cursor': 'cursor',
    'mode-line': 'modeline',
    'mode-line-buffer-id': 'entity.name.tag.doctype',
    'mode-line-emphasis': 'markup.bold',
    'mode-line-highlight': 'markup.italic',
    'mode-line-inactive': 'modeline.inactive',
    'isearch': 'markup.changed.diff',
    'isearch-fail': 'markup.deleted.diff',
    'iswitchb-current-match': 'markup.inserted.diff',
    'iswitchb-invalid-regexp': 'markup.deleted.diff',
    'iswitchb-single-match': 'markup.inserted.diff',
    'iswitchb-virtual-matches': 'markup.changed.diff',
    'match': 'markup.inserted.diff',
    'menu': 'default',
    'minibuffer-prompt': 'unknown',
    'hl-line': 'highline',
    'fringe': 'highline',
    'linum': 'highline',
    'border': 'highline',
    'region': 'selection',
    'secondary-selection': 'selection',
    'highlight': 'selection',
    'minibuffer-prompt': 'meta.tag',
    'font-lock-builtin-face': 'support.function',
    'font-lock-comment-face': 'comment',
    'font-lock-comment-delimiter-face': 'comment',
    'font-lock-constant-face': 'support.constant',
    'font-lock-doc-face': 'string constant',
    'font-lock-function-name-face': 'support.function',
    'font-lock-keyword-face': 'keyword',
    'font-lock-negation-char-face': 'keyword.operator',
    'font-lock-preprocessor-face': 'meta.preprocessor.c',
    'font-lock-regexp-grouping-construct': 'string.regexp string.regexp.arbitrary-repitition',
    'font-lock-regexp-grouping-backslash': 'string.regexp constant.character.escape',
    'font-lock-string-face': 'string constant',
    'font-lock-type-face': 'storage.type',
    'font-lock-variable-name-face': 'variable.other.normal',
    'font-lock-warning-face': 'invalid.illegal',
    'diff-header': 'meta.diff.header',
    'diff-hunk-header': 'meta.diff.header',
    'diff-removed': 'markup.deleted.diff',
    'diff-changed': 'markup.changed.diff',
    'diff-added': 'markup.inserted.diff',
    'show-paren-match-face': 'selection',
    'show-paren-mismatch-face': 'invalid.illegal',

    'css-property': 'support.type.property-name.css',
    'css-proprietary-property': 'support.type.property-name.css',
    'css-selector': 'meta.selector.css entity.other.attribute-name.id',

    #org-agenda-structure': '',
    #org-agenda-date': '',
    #org-agenda-done': '',
    'org-agenda-dimmed-todo-face': 'comment',
    #org-block': '',
    #org-code': '',
    'org-column': 'highline',
    'org-column-title': 'highline',
    #org-date': '',
    #org-document-info': '',
    #org-document-info-keyword': '',
    #org-document-title': '',
    'org-done': 'markup.inserted.diff',
    #org-ellipsis': '',
    #org-footnote': '',
    #org-formula': '',
    'org-hide': 'hide',
    'org-link': 'meta.link.inline markup.underline',
    #'org-scheduled': '',
    #'org-scheduled-previously': '',
    #'org-scheduled-today': '',
    #'org-special-keyword': '',
    #'org-table': '',
    'org-todo': 'invalid.illegal',
    'org-upcoming-deadline': 'invalid.deprecated',
    'org-warning': 'invalid.illegal'
}

# alpha_blend(hex_color, emacs dict)
#    If the hex color has an alpha channel, return an alpha-blended
#    combination of the color with the default background. Otherwise,
#    just return the color.
def alpha_blend(color, emlist):
    rgb = np.array(matplotlib.colors.hex2color(color[0:7]))
    if color[7:] == '':
        alpha = 1.0
    else:
        alpha = int(color[7:], 16) / 255.
    bg = np.array(matplotlib.colors.hex2color(emlist['default']['background']))
    blend = tuple(bg*(1. - alpha) + rgb*alpha)
    return matplotlib.colors.rgb2hex(blend)

# tmToEmProps(scope, emacs dict)
#    Convert the props in the dict indexed by scope to a string specifying
#    the equivalent emacs face properties
def tmToEmProps(scope, emlist):
    s = ""
    for prop, val in emlist[scope].iteritems():
        if prop == "background":
            s = s+":background \""+alpha_blend(val, emlist)+"\" "
        elif prop == "foreground":
            s = s+":foreground \""+alpha_blend(val, emlist)+"\" "
        elif prop == "box":
            s = s+":box \""+val+"\" "
        elif prop == "fontStyle":
            for attr in [x for x in val.split(' ') if x]:
                s = s+":"+attr+ " t "
    return s

# genscope(scope)
#    generator to return containing scopes
def genscope(scope):
    rscope=scope[::-1]
    yield scope
    res=re.split('[. ]', rscope, 1)
    while len(res) > 1:
        rscope=res[1]
        res=re.split('[. ]', rscope, 1)
        yield rscope[::-1]

def createStandardFace(face,scope,emlist,out,prefix):
    for hscope in genscope(scope):
        if hscope in emlist:
            print(faceformat % (prefix,face,tmToEmProps(hscope, emlist)), file=out)
            return
    if debug:
        print( "No face found for %s in scope %s" % (face,scope))

# createStandardFaces(dict, file)
#    For each face in the emtotm dictionary, if there is a matching scope
#    in dict, output an appropriate face theme spec
def createStandardFaces(emlist, out, prefix):
    for face, scope in iter(sorted(emtotm.iteritems())):
        createStandardFace(face,scope,emlist,out,prefix)

def sanitizeName(name):
    sanity = re.sub('[\s]+', '-', name)
    sanity = re.sub('[^A-Za-z0-9-]', '', sanity)
    return sanity.lower()

def exit_usage(parser):
    parser.print_help()
    sys.exit(1)

usage = "usage: %prog [options] <theme>"
parser = OptionParser(usage=usage)
parser.add_option("-n", action="store_true", dest="newStyle", 
                  help="generate emacs 24 style themes", default=False)
parser.add_option("-d", action="store_true", dest="debug",
                  help="Generate additional diagnostic information", default=False)
parser.add_option("-D", action="store", dest="outputDir", default=None,
                  help="Directory to store output in")

(options, args) = parser.parse_args()

debug=options.debug

if len(args) != 1:
    exit_usage(parser)

tmFile=args[0]

# initialize the dictionaries
em = dict()
em['default'] = dict()

# Overridable defaults
em['markup.bold'] = { 'fontStyle': 'bold' }
em['markup.italic'] = { 'fontStyle': 'italic' }
em['markup.underline'] = { 'fontStyle': 'underline' }

tm = readPlist(tmFile)

themeName = sanitizeName(tm['name'])

# massage the format of the settings from the tm dictionary into
# a more suitable format
for i,v in enumerate(tm["settings"]):
    if "scope" in v:
        contexts = [x.strip() for x in v['scope'].split(',')]
        for context in contexts:
            em[context] = v['settings']
    else:
        em['default'] = dict(v['settings'].items() + em['default'].items())

# Fix the background if it's got an alpha channel
if em['default']['background'][7:] != '':
    alphabg=em['default']['background']
    em['default']['background'] = "#000000"
    em['default']['background'] = alpha_blend(alphabg, em)

# meta styles for some specialty faces
em['cursor'] = { 'background' : em['default']['caret'] }
em['selection'] = {'background': em['default']['selection'] }
em['highline'] = {'background': em['default']['lineHighlight']}
em['hide'] = { 'foreground': em['default']['background']}

# textMate themes don't style the UI, so set up some equivalents
# for the modeline
if 'meta.separator' in em:
    em['modeline'] = em['meta.separator']
    em['modeline.inactive'] = {'background': em['default']['selection']}
    if 'selectionBorder' in em['default']: 
        em['modeline.inactive']['box'] = em['default']['selectionBorder']
else:
    em['modeline'] = {'background': em['default']['selection']}
    em['modeline.inactive'] = {'background': em['default']['lineHighlight']}
    if 'selectionBorder' in em['default']: 
        em['modeline.inactive']['box'] = em['default']['selectionBorder']
        em['modeline']['box'] = em['default']['selectionBorder']

if options.newStyle:
    emFile = options.outputDir+"/"+themeName+"-theme.el"
else:
    emFile = options.outputDir+"/color-theme-"+themeName+".el"

out = open(emFile, 'w')

# For reference, dump the settings in a comment block at the start
for i,v in enumerate(tm["settings"]):
    if "scope" in v:
        print(";; %s %s" % (str(v['scope']), str(v['settings'])), file=out)
    else:
        print(";; %s %s" %("default", str(v['settings'])), file=out)

author=tm['author'] if 'author' in tm else "unknown"
name=tm['name']
desc="%s emacs theme autoconverted from the textMate theme by %s" % (name, author)

# Generate the file
if options.newStyle:
    prefix="'"
else:
    prefix=""

if options.newStyle:
    print('(deftheme %s' % themeName, file=out)
    print('  "%s")' % desc, file=out)
    print('(custom-theme-set-faces', file=out)
    print("  '%s" % themeName, file=out)
else:
    print("(eval-when-compile (require 'color-theme))", file=out)
    print('(defun color-theme-%s ()' % themeName, file=out)
    print('  "%s"' % desc, file=out)
    print('  (interactive)', file=out)
    print('  (color-theme-install', file=out)
    print('   `(color-theme-%s' % themeName, file=out)
    print('    ((background-color . "%s")' % em['default']['background'], file=out)
    print('     (foreground-color . "%s")' % em['default']['foreground'], file=out)
    print('     (cursor-color . "%s")' % em['default']['caret'], file=out)
if 'invisibles' in em['default']:
    print(faceformat % (prefix, 'trailing-whitespace', ':background "'+alpha_blend(em['default']['invisibles'], em)+'"'), file=out)
    print(faceformat % (prefix, 'whitespace-trailing',':background "'+alpha_blend(em['default']['invisibles'], em)+'"'), file=out)
    print('', file=out)
createStandardFaces(em, out, prefix)
if options.newStyle:
    print(')', file=out)
    print('', file=out)
    print("(provide-theme '%s)" % themeName, file=out)
else:
    print('    )))', file=out)
    print("(provide 'color-theme-%s)" % themeName, file=out)

out.close()
